/*
 * Copyright 2021 GFXFundamentals.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of GFXFundamentals. nor the names of his
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/* global define */

(function(root, factory) {  // eslint-disable-line
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define([], function() {
      return factory.call(root);
    });
  } else {
    // Browser globals
    root.webglDebugHelper = factory.call(root);
  }
}(this, function() {
  'use strict';  // eslint-disable-line

  //------------ [ from https://github.com/KhronosGroup/WebGLDeveloperTools ]

  /*
  ** Copyright (c) 2012 The Khronos Group Inc.
  **
  ** Permission is hereby granted, free of charge, to any person obtaining a
  ** copy of this software and/or associated documentation files (the
  ** "Materials"), to deal in the Materials without restriction, including
  ** without limitation the rights to use, copy, modify, merge, publish,
  ** distribute, sublicense, and/or sell copies of the Materials, and to
  ** permit persons to whom the Materials are furnished to do so, subject to
  ** the following conditions:
  **
  ** The above copyright notice and this permission notice shall be included
  ** in all copies or substantial portions of the Materials.
  **
  ** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  ** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  ** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  ** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
  ** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
  ** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
  */

  /**
   * Types of contexts we have added to map
   */
  const mappedContextTypes = {};

  /**
   * Map of numbers to names.
   * @type {Object}
   */
  const glEnums = {};

  /**
   * Map of names to numbers.
   * @type {Object}
   */
  const enumStringToValue = {};

  /**
   * Initializes this module. Safe to call more than once.
   * @param {!WebGLRenderingContext} ctx A WebGL context. If
   *    you have more than one context it doesn't matter which one
   *    you pass in, it is only used to pull out constants.
   */
  function addEnumsForContext(ctx, type) {
    if (!mappedContextTypes[type]) {
      mappedContextTypes[type] = true;
      for (const propertyName in ctx) {
        if (typeof ctx[propertyName] === 'number') {
          glEnums[ctx[propertyName]] = propertyName;
          enumStringToValue[propertyName] = ctx[propertyName];
        }
      }
    }
  }

  function enumArrayToString(enums) {
    const enumStrings = [];
    if (enums.length) {
      for (let i = 0; i < enums.length; ++i) {
        enums.push(glEnumToString(enums[i]));  // eslint-disable-line
      }
      return '[' + enumStrings.join(', ') + ']';
    }
    return enumStrings.toString();
  }

  function makeBitFieldToStringFunc(enums) {
    return function(value) {
      let orResult = 0;
      const orEnums = [];
      for (let i = 0; i < enums.length; ++i) {
        const enumValue = enumStringToValue[enums[i]];
        if ((value & enumValue) !== 0) {
          orResult |= enumValue;
          orEnums.push(glEnumToString(enumValue));  // eslint-disable-line
        }
      }
      if (orResult === value) {
        return orEnums.join(' | ');
      } else {
        return glEnumToString(value);  // eslint-disable-line
      }
    };
  }

  const destBufferBitFieldToString = makeBitFieldToStringFunc([
    'COLOR_BUFFER_BIT',
    'DEPTH_BUFFER_BIT',
    'STENCIL_BUFFER_BIT',
  ]);

  /**
   * Which arguments are enums based on the number of arguments to the function.
   * So
   *    'texImage2D': {
   *       9: { 0:true, 2:true, 6:true, 7:true },
   *       6: { 0:true, 2:true, 3:true, 4:true },
   *    },
   *
   * means if there are 9 arguments then 6 and 7 are enums, if there are 6
   * arguments 3 and 4 are enums. Maybe a function as well in which case
   * value is passed to function and returns a string
   *
   * @type {!Object.<number, (!Object.<number, string>|function)}
   */
  const glValidEnumContexts = {
    // Generic setters and getters

    'enable': {1: { 0:true }},
    'disable': {1: { 0:true }},
    'getParameter': {1: { 0:true }},

    // Rendering

    'drawArrays': {3:{ 0:true }},
    'drawElements': {4:{ 0:true, 2:true }},
    'drawArraysInstanced': {4: { 0:true }},
    'drawElementsInstanced': {5: {0:true, 2: true }},
    'drawRangeElements': {6: {0:true, 4: true }},

    // Shaders

    'createShader': {1: { 0:true }},
    'getShaderParameter': {2: { 1:true }},
    'getProgramParameter': {2: { 1:true }},
    'getShaderPrecisionFormat': {2: { 0: true, 1:true }},

    // Vertex attributes

    'getVertexAttrib': {2: { 1:true }},
    'vertexAttribPointer': {6: { 2:true }},
    'vertexAttribIPointer': {5: { 2:true }},  // WebGL2

    // Textures

    'bindTexture': {2: { 0:true }},
    'activeTexture': {1: { 0:true }},
    'getTexParameter': {2: { 0:true, 1:true }},
    'texParameterf': {3: { 0:true, 1:true }},
    'texParameteri': {3: { 0:true, 1:true, 2:true }},
    'texImage2D': {
      9: { 0:true, 2:true, 6:true, 7:true },
      6: { 0:true, 2:true, 3:true, 4:true },
      10: { 0:true, 2:true, 6:true, 7:true },  // WebGL2
    },
    'texImage3D': {
      10: { 0:true, 2:true, 7:true, 8:true },  // WebGL2
      11: { 0:true, 2:true, 7:true, 8:true },  // WebGL2
    },
    'texSubImage2D': {
      9: { 0:true, 6:true, 7:true },
      7: { 0:true, 4:true, 5:true },
      10: { 0:true, 6:true, 7:true },  // WebGL2
    },
    'texSubImage3D': {
      11: { 0:true, 8:true, 9:true },  // WebGL2
      12: { 0:true, 8:true, 9:true },  // WebGL2
    },
    'texStorage2D': { 5: { 0:true, 2:true }},  // WebGL2
    'texStorage3D': { 6: { 0:true, 2:true }},  // WebGL2
    'copyTexImage2D': {8: { 0:true, 2:true }},
    'copyTexSubImage2D': {8: { 0:true }},
    'copyTexSubImage3D': {9: { 0:true }},  // WebGL2
    'generateMipmap': {1: { 0:true }},
    'compressedTexImage2D': {
      7: { 0: true, 2:true },
      8: { 0: true, 2:true },  // WebGL2
    },
    'compressedTexSubImage2D': {
      8: { 0: true, 6:true },
      9: { 0: true, 6:true },  // WebGL2
    },
    'compressedTexImage3D': {
      8: { 0: true, 2: true, },  // WebGL2
      9: { 0: true, 2: true, },  // WebGL2
    },
    'compressedTexSubImage3D': {
      9: { 0: true, 8: true, },  // WebGL2
      10: { 0: true, 8: true, },  // WebGL2
    },

    // Buffer objects

    'bindBuffer': {2: { 0:true }},
    'bufferData': {
      3: { 0:true, 2:true },
      4: { 0:true, 2:true },  // WebGL2
      5: { 0:true, 2:true },  // WebGL2
    },
    'bufferSubData': {
      3: { 0:true },
      4: { 0:true },  // WebGL2
      5: { 0:true },  // WebGL2
    },
    'copyBufferSubData': {
      5: { 0:true },  // WeBGL2
    },
    'getBufferParameter': {2: { 0:true, 1:true }},
    'getBufferSubData': {
      3: { 0: true, },  // WebGL2
      4: { 0: true, },  // WebGL2
      5: { 0: true, },  // WebGL2
    },

    // Renderbuffers and framebuffers

    'pixelStorei': {2: { 0:true, 1:true }},
    'readPixels': {
      7: { 4:true, 5:true },
      8: { 4:true, 5:true },  // WebGL2
    },
    'bindRenderbuffer': {2: { 0:true }},
    'bindFramebuffer': {2: { 0:true }},
    'blitFramebuffer': {10: { 8: destBufferBitFieldToString, 9:true }},  // WebGL2
    'checkFramebufferStatus': {1: { 0:true }},
    'framebufferRenderbuffer': {4: { 0:true, 1:true, 2:true }},
    'framebufferTexture2D': {5: { 0:true, 1:true, 2:true }},
    'framebufferTextureLayer': {5: {0:true, 1:true }},  // WebGL2
    'getFramebufferAttachmentParameter': {3: { 0:true, 1:true, 2:true }},
    'getInternalformatParameter': {3: {0:true, 1:true, 2:true }},  // WebGL2
    'getRenderbufferParameter': {2: { 0:true, 1:true }},
    'invalidateFramebuffer': {2: { 0:true, 1: enumArrayToString, }},  // WebGL2
    'invalidateSubFramebuffer': {6: {0: true, 1: enumArrayToString, }},  // WebGL2
    'readBuffer': {1: {0: true}},  // WebGL2
    'renderbufferStorage': {4: { 0:true, 1:true }},
    'renderbufferStorageMultisample': {5: { 0: true, 2: true }},  // WebGL2

    // Frame buffer operations (clear, blend, depth test, stencil)

    'clear': {1: { 0: destBufferBitFieldToString }},
    'depthFunc': {1: { 0:true }},
    'blendFunc': {2: { 0:true, 1:true }},
    'blendFuncSeparate': {4: { 0:true, 1:true, 2:true, 3:true }},
    'blendEquation': {1: { 0:true }},
    'blendEquationSeparate': {2: { 0:true, 1:true }},
    'stencilFunc': {3: { 0:true }},
    'stencilFuncSeparate': {4: { 0:true, 1:true }},
    'stencilMaskSeparate': {2: { 0:true }},
    'stencilOp': {3: { 0:true, 1:true, 2:true }},
    'stencilOpSeparate': {4: { 0:true, 1:true, 2:true, 3:true }},

    // Culling

    'cullFace': {1: { 0:true }},
    'frontFace': {1: { 0:true }},

    // ANGLE_instanced_arrays extension

    'drawArraysInstancedANGLE': {4: { 0:true }},
    'drawElementsInstancedANGLE': {5: { 0:true, 2:true }},

    // EXT_blend_minmax extension

    'blendEquationEXT': {1: { 0:true }},

    // Multiple Render Targets

    'drawBuffersWebGL': {1: {0: enumArrayToString, }},  // WEBGL_draw_bufers
    'drawBuffers': {1: {0: enumArrayToString, }},  // WebGL2
    'clearBufferfv': {
      4: {0: true },  // WebGL2
      5: {0: true },  // WebGL2
    },
    'clearBufferiv': {
      4: {0: true },  // WebGL2
      5: {0: true },  // WebGL2
    },
    'clearBufferuiv': {
      4: {0: true },  // WebGL2
      5: {0: true },  // WebGL2
    },
    'clearBufferfi': { 4: {0: true}},  // WebGL2

    // QueryObjects

    'beginQuery': { 2: { 0: true }},  // WebGL2
    'endQuery': { 1: { 0: true }},  // WebGL2
    'getQuery': { 2: { 0: true, 1: true }},  // WebGL2
    'getQueryParameter': { 2: { 1: true }},  // WebGL2

    //  Sampler Objects

    'samplerParameteri': { 3: { 1: true }},  // WebGL2
    'samplerParameterf': { 3: { 1: true }},  // WebGL2
    'getSamplerParameter': { 2: { 1: true }},  // WebGL2

    //  Sync objects

    'clientWaitSync': { 3: { 1: makeBitFieldToStringFunc(['SYNC_FLUSH_COMMANDS_BIT']) }},  // WebGL2
    'fenceSync': { 2: { 0: true }},  // WebGL2
    'getSyncParameter': { 2: { 1: true }},  // WebGL2

    //  Transform Feedback

    'bindTransformFeedback': { 2: { 0: true }},  // WebGL2
    'beginTransformFeedback': { 1: { 0: true }},  // WebGL2

    // Uniform Buffer Objects and Transform Feedback Buffers
    'bindBufferBase': { 3: { 0: true }},  // WebGL2
    'bindBufferRange': { 5: { 0: true }},  // WebGL2
    'getIndexedParameter': { 2: { 0: true }},  // WebGL2
    'getActiveUniforms': { 3: { 2: true }},  // WebGL2
    'getActiveUniformBlockParameter': { 3: { 2: true }},  // WebGL2
  };

  /**
   * Gets an string version of an WebGL enum.
   *
   * Example:
   *   var str = WebGLDebugUtil.glEnumToString(ctx.getError());
   *
   * @param {number} value Value to return an enum for
   * @return {string} The string version of the enum.
   */
  function glEnumToString(value) {
    const name = glEnums[value];
    return (name !== undefined)
        ? `gl.${name}`
        : `/*UNKNOWN WebGL ENUM*/ ${typeof value === 'number' ? `0x${value.toString(16)}` : value}`;
  }

  /**
   * Returns the string version of a WebGL argument.
   * Attempts to convert enum arguments to strings.
   * @param {string} functionName the name of the WebGL function.
   * @param {number} numArgs the number of arguments passed to the function.
   * @param {number} argumentIndx the index of the argument.
   * @param {*} value The value of the argument.
   * @return {string} The value as a string.
   */
  function glFunctionArgToString(functionName, numArgs, argumentIndex, value) {
    const funcInfos = glValidEnumContexts[functionName];
    if (funcInfos !== undefined) {
      const funcInfo = funcInfos[numArgs];
      if (funcInfo !== undefined) {
        const argType = funcInfo[argumentIndex];
        if (argType) {
          if (typeof argType === 'function') {
            return argType(value);
          } else {
            return glEnumToString(value);
          }
        }
      }
    }
    if (value === null) {
      return 'null';
    } else if (value === undefined) {
      return 'undefined';
    } else if (Array.isArray(value)) {
      return `[${value.toString()}]`;
    } else if (typeof value.length === 'number' && value.buffer && value.buffer instanceof ArrayBuffer) {
      return `${value.constructor.name}(${value.length})`;
    } else {
      return value.toString();
    }
  }

  /**
   * Converts the arguments of a WebGL function to a string.
   * Attempts to convert enum arguments to strings.
   *
   * @param {string} functionName the name of the WebGL function.
   * @param {number} args The arguments.
   * @return {string} The arguments as a string.
   */
  function glFunctionArgsToString(functionName, args) {
    // apparently we can't do args.join(",");
    const argStrs = [];
    const numArgs = args.length;
    for (let ii = 0; ii < numArgs; ++ii) {
      argStrs.push(glFunctionArgToString(functionName, numArgs, ii, args[ii]));
    }
    return argStrs.join(', ');
  }

  /**
   * Given a WebGL context returns a wrapped context that calls
   * gl.getError after every command and calls a function if the
   * result is not gl.NO_ERROR.
   *
   * @param {!WebGLRenderingContext} ctx The webgl context to
   *        wrap.
   * @param {!function(err, funcName, args): void} opt_onErrorFunc
   *        The function to call when gl.getError returns an
   *        error. If not specified the default function calls
   *        console.log with a message.
   * @param {!function(funcName, args): void} opt_onFunc The
   *        function to call when each webgl function is called.
   *        You can use this to log all calls for example.
   * @param {!WebGLRenderingContext} opt_err_ctx The webgl context
   *        to call getError on if different than ctx.
   */
  function makeDebugContext(ctx, options) {
    options = options || {};
    const onFunc = options.funcFunc;
    const sharedState = options.sharedState || {
      numDrawCallsRemaining: options.maxDrawCalls || -1,
      wrappers: {},
      restoreFns: [],
    };

    const errorFunc = options.errorFunc || function(err, functionName, args) {
      console.error(`WebGL error ${glEnumToString(err)} in ${functionName}(${glFunctionArgsToString(functionName, args)})`);  /* eslint-disable-line no-console */
    };

    // Holds booleans for each GL error so after we get the error ourselves
    // we can still return it to the client app.
    const glErrorShadow = { };

    function removeChecks() {
      sharedState.restoreFns.forEach(fn => fn());
      sharedState.restoreFns = [];
    }

    function checkMaxDrawCalls() {
      if (sharedState.numDrawCallsRemaining === 0) {
        removeChecks();
      }
      --sharedState.numDrawCallsRemaining;
    }

    function noop() {
    }

    // Makes a function that calls a WebGL function and then calls getError.
    function makeErrorWrapper(ctx, functionName) {
      const check = functionName.substring(0, 4) === 'draw' ? checkMaxDrawCalls : noop;
      const origFn = ctx[functionName];
      const newFn = function(...args) {
        if (onFunc) {
          onFunc(functionName, args);
        }
        const result = origFn.call(this, ...args);
        const err = sharedState.getError();
        if (err !== 0) {
          glErrorShadow[err] = true;
          errorFunc(err, functionName, args);
        }
        check();
        return result;
      };
      ctx[functionName] = newFn;
      sharedState.restoreFns.unshift(function() {
        ctx[functionName] = origFn;
      });
    }

    function makeGetExtensionWrapper(ctx) {
      const origFn = ctx.getExtension;
      ctx.getExtension = function(extensionName) {
        let ext = sharedState.wrappers[extensionName];
        if (!ext) {
          ext = origFn.call(this, extensionName);
          if (ext) {
            ext = makeDebugContext(ext, {...options, sharedState});
            sharedState.wrappers[extensionName] = ext;
            addEnumsForContext(ext, extensionName);
          }
        }
        return ext;
      };
      sharedState.restoreFns.unshift(function() {
        ctx.getExtension = origFn;
      });
    }

    function makeGetErrorWrapper(ctx) {
      const origFn = ctx.getError;
      sharedState.getError = function() {
        return origFn.call(ctx);
      };
      ctx.getError = function() {
        for (const err of Object.keys(glErrorShadow)) {
          if (glErrorShadow[err]) {
            glErrorShadow[err] = false;
            return err;
          }
        }
        return ctx.NO_ERROR;
      };
      sharedState.restoreFns.unshift(function() {
        ctx.getError = origFn;
      });
    }

    // Make a an object that has a copy of every property of the WebGL context
    // but wraps all functions.
    for (const propertyName in ctx) {
      if (typeof ctx[propertyName] === 'function') {
        switch (propertyName) {
          case 'getExtension':
            makeGetExtensionWrapper(ctx);
            break;
          case 'getError':
            makeGetErrorWrapper(ctx);
            break;
          default:
            makeErrorWrapper(ctx, propertyName);
        }
      }
    }

    if (ctx.bindBuffer) {
      sharedState.wrappers['webgl'] = ctx;
      addEnumsForContext(ctx, ctx.bindBufferBase ? 'WebGL2' : 'WebGL');
    }

    return ctx;
  }

  return {
    makeDebugContext,
    glFunctionArgsToString,
    glFunctionArgToString,
    glEnumToString,
  };

}));

